<?php

namespace yunj\builder;

use yunj\Config;
use yunj\Validate;
use yunj\enum\BuilderType;

final class YunjImport extends Builder {

    protected $type = BuilderType::IMPORT;

    protected $builderValidateClass = "\\yunj\\validate\\YunjImport";

    protected $scriptFileList = ["/static/yunj/js/import.min.js?v=" . YUNJ_VERSION];

    protected $options = ["sheet", "cols", "colsValidate", "limit", "tips", "row", "rows", "validate"];

    /**
     * 工作表
     * "sheet"=>["Sheet1","Sheet2"]
     * @var array
     */
    private $sheet;

    /**
     * 工作表表头
     * @var array<col-key,col-args>|array<sheet,array<col-key,col-args>>
     * 示例：sheet没有设置时   array<col-key,col-args>
     * 'cols'=>[
     *      "name"=>[
     *          "title"=>"姓名",
     *          "default"=>"小王",
     *          "verify"=>"require|chs",
     *          "desc"=>"必填，只能输入汉字",
     *      ],...
     *  ]
     * 示例：sheet设置时    array<sheet,array<col-key,col-args>>
     * "cols"=>[
     *      "Sheet1"=>[
     *          "name"=>[
     *              "title"=>"姓名",
     *              "default"=>"小王",
     *              "verify"=>"require|chs",
     *              "desc"=>"必填，只能输入汉字",
     *          ],...
     *      ],...
     * ]
     */
    private $cols;

    /**
     * 工作表表头验证的回调方法
     * @var callable
     */
    private $colsValidateCallback;

    /**
     * 每次导入条数，默认20条
     * "limit"=>20
     * @var int
     */
    private $limit = 20;

    /**
     * 提示语
     * "tips"=>[
     *      "提示一",
     *      "提示二",
     * ]
     * @var array
     */
    private $tips = [];

    // 单行数据处理回调
    private $rowCallback;

    // 多行数据处理回调
    private $rowsCallback;

    protected function setAttr(array $args = []): void {
        parent::setAttr($args);
        $this->config = Config::get('import.', []);
    }

    /**
     * Notes: 工作表
     * Author: Uncle-L
     * Date: 2021/4/10
     * Time: 17:21
     * @param callable|array<string>|...string $sheet
     * callable:
     *      function(){
     *          return ["Sheet1","Sheet2"];
     *      }
     * array<string>:
     *      ["Sheet1","Sheet2"];
     * ...string:
     *      "Sheet1","Sheet2"
     * @return YunjImport
     */
    public function sheet($sheet) {
        if ($this->existError()) return $this;
        if (!$sheet) return $this;
        $sheet0 = $sheet[0];
        if (is_callable($sheet0)) {
            $callable = $sheet0;
        } elseif (is_array($sheet0)) {
            $callable = function () use ($sheet0) {
                return $sheet0;
            };
        } else {
            $callable = function () use ($sheet) {
                return $sheet;
            };
        }
        $this->setOptionCallbale('sheet', $callable);
        return $this;
    }

    /**
     * Notes: 执行工作表回调
     * Author: Uncle-L
     * Date: 2021/10/8
     * Time: 18:33
     * @param callable $callable
     * @return YunjImport
     */
    protected function exec_sheet_callable(callable $callable) {
        if ($this->existError()) return $this;
        $this->sheet = $this->sheet ?: [];
        $sheet = $callable();
        if (!is_array($sheet)) return $this->error("YunjForm [sheet] 需返回有效数组");
        $this->sheet = array_keys(array_flip($this->sheet) + array_flip($sheet));
        return $this;
    }

    /**
     * Notes: 判断是否设置sheet
     * Author: Uncle-L
     * Date: 2022/3/8
     * Time: 17:30
     * @return bool
     */
    private function isSetSheet(): bool {
        $this->execOptionsCallbale("sheet");
        return $this->sheet && count($this->sheet) > 0;
    }

    /**
     * Notes: 工作表表头
     * Author: Uncle-L
     * Date: 2021/4/10
     * Time: 17:23
     * @param callable|array<col-key,col-args> $cols
     * @return YunjImport
     */
    public function cols($cols) {
        if ($this->existError()) return $this;
        if (!is_callable($cols)) {
            $callable = function () use ($cols) {
                return $cols;
            };
        } else {
            $callable = $cols;
        }
        $this->setOptionCallbale('cols', $callable);
        return $this;
    }

    /**
     * Notes: 执行工作表表头回调
     * Author: Uncle-L
     * Date: 2021/3/29
     * Time: 10:10
     * @param callable $callable
     * @return YunjImport
     */
    protected function exec_cols_callable(callable $callable) {
        if ($this->existError()) return $this;
        $sheets = $this->sheet;
        $cols = $this->cols ?: [];
        if ($sheets) {
            foreach ($sheets as $sheet) {
                $sheetCols = $callable($sheet);
                if (!is_array($sheetCols)) return $this->error("YunjImport [cols] 需返回有效数组");
                $cols[$sheet] = isset($cols[$sheet]) ? $cols[$sheet] : [];
                foreach ($sheetCols as $key => $args) {
                    $cols[$sheet][$key] = $args + ["title" => "", "default" => "", "verify" => "", "desc" => ""];
                }
            }
        } else {
            $_cols = $callable(null);
            if (!is_array($_cols)) return $this->error("YunjImport [cols] 需返回有效数组");
            foreach ($_cols as $key => $args) {
                $cols[$key] = $args + ["title" => "", "default" => "", "verify" => "", "desc" => ""];
            }
        }
        $this->cols = $cols;
        return $this;
    }

    /**
     * Notes: 工作表表头的验证器
     * Author: Uncle-L
     * Date: 2021/3/29
     * Time: 10:05
     * @param callable|string<Validate-class-name> $colsValidate
     * callable:
     *      function(){
     *          // 返回验证器全限定类名
     *          return \\demo\\validate\\TestValidate::class;
     *          // 返回验证器示例
     *          return new TestValidate();
     *      }
     * string:
     *      \\demo\\validate\\TestValidate::class
     * @return YunjImport
     */
    public function colsValidate($colsValidate) {
        if ($this->existError()) return $this;
        if (!is_callable($colsValidate)) {
            $callable = function () use ($colsValidate) {
                return $colsValidate;
            };
        } else {
            $callable = $colsValidate;
        }
        $this->colsValidateCallback = $callable;
        return $this;
    }

    /**
     * Notes: 每次导入条数
     * Author: Uncle-L
     * Date: 2021/4/13
     * Time: 19:40
     * @param callable|int $limit
     * callable:
     *      function(){
     *          return 20;
     *      }
     * int:
     *      20
     * @return YunjImport
     */
    public function limit($limit) {
        if ($this->existError()) return $this;
        if (!is_callable($limit)) {
            $callable = function () use ($limit) {
                return $limit;
            };
        } else {
            $callable = $limit;
        }
        $this->setOptionCallbale('limit', $callable);
        return $this;
    }

    /**
     * Notes: 执行每次导入条数回调
     * Author: Uncle-L
     * Date: 2021/10/8
     * Time: 18:33
     * @param callable $callable
     * @return YunjImport
     */
    protected function exec_limit_callable(callable $callable) {
        if ($this->existError()) return $this;
        $limit = $callable();
        if (!is_positive_integer($limit)) return $this->error("YunjImport [limit] 需返回正整数");
        $this->limit = $limit;
        return $this;
    }

    /**
     * Notes: 提示语
     * Author: Uncle-L
     * Date: 2021/4/10
     * Time: 17:22
     * @param callable|array<string>|...string $tips
     * callable:
     *      function(){
     *          return ["提示一","提示二"];
     *      }
     * array<string>:
     *      ["提示一","提示二"]
     * ...string:
     *      "提示一","提示二"
     * @return YunjImport
     */
    public function tips(...$tips) {
        if ($this->existError()) return $this;
        if (!$tips) return $this;
        $tip0 = $tips[0];
        if (is_callable($tip0)) {
            $callable = $tip0;
        } elseif (is_array($tip0)) {
            $callable = function () use ($tip0) {
                return $tip0;
            };
        } else {
            $callable = function () use ($tips) {
                return $tips;
            };
        }
        $this->setOptionCallbale('tips', $callable);
        return $this;
    }

    /**
     * Notes: 执行提示语回调
     * Author: Uncle-L
     * Date: 2021/10/8
     * Time: 18:33
     * @param callable $callable
     * @return YunjImport
     */
    protected function exec_tips_callable(callable $callable) {
        if ($this->existError()) return $this;
        $this->tips = $this->tips ?: [];
        $tips = $callable();
        if (!is_array($tips) || !array_depth($tips, 1)) return $this->error("YunjImport [tips] 需返回一维数组");
        $this->tips = array_keys(array_flip($this->tips) + array_flip($tips));
        return $this;
    }

    /**
     * Notes: 单行数据处理
     * Author: Uncle-L
     * Date: 2021/3/29
     * Time: 11:04
     * @param callable $callable
     * @return YunjImport
     */
    public function row(callable $callable) {
        if ($this->existError()) return $this;
        $this->rowCallback = $callable;
        return $this;
    }

    /**
     * Notes: 多行数据处理
     * Author: Uncle-L
     * Date: 2021/3/29
     * Time: 11:04
     * @param callable $callable
     * @return YunjImport
     */
    public function rows(callable $callable) {
        if ($this->existError()) return $this;
        $this->rowsCallback = $callable;
        return $this;
    }

    public function viewArgs() {
        $args = parent::viewArgs();
        if ($this->sheet) {
            $args['sheet'] = $this->sheet;
        }
        if ($this->cols) {
            $args['cols'] = $this->cols;
        }
        $args['limit'] = $this->limit;
        $args['tips'] = array_keys(array_flip($this->config["tips"]) + array_flip($this->tips));
        return $args;
    }

    /**
     * Notes: 异步数据导入
     * Author: Uncle-L
     * Date: 2021/10/12
     * Time: 16:34
     * @param array $data
     * @return array
     */
    protected function async_import(array $data) {
        $this->execOptionsCallbale("sheet,cols,colsValidate,limit,row,rows");
        if ($this->existError()) throw_error_json($this->getError());
        if (!$this->rowCallback && !$this->rowsCallback) throw_error_json("YunjImport [row]或[rows] 未设置");
        if ($data["count"] > $this->limit) throw_error_json("请求数量超过限制{$this->limit}");
        $validate = $this->colsValidateCallback ? call_user_func($this->colsValidateCallback) : (new Validate());
        if (is_string($validate)) $validate = class_exists($validate) ? (new $validate()) : null;
        if (!$validate || !($validate instanceof Validate)) throw_error_json("YunjImport [colsValidate] 需返回 \\yunj\\Validate及其子类 的实例对象或全限定类名");

        $items = $data['items'];
        $passItems = [];
        $passIdKeyMap = [];
        $isSetSheet = $this->isSetSheet();
        foreach ($items as $k => $v) {
            $sheet = $isSetSheet ? $v['sheet'] : null;
            $id = $v['id'];
            $row = $v['row'];
            $res = $this->async_import_validate($validate, $row, $sheet);
            if ($res !== true) {
                $items[$k]["state"]["code"] = "fail";
                $items[$k]["state"]["tips"] = "上传失败：" . $res;
                continue;
            }
            if ($this->rowCallback) {
                $res = call_user_func_array($this->rowCallback, [$row, $sheet]);
                $this->setImportItemState($items[$k], $res === true ? null : (is_string($res) ? $res : ""));
                continue;
            }
            $v["row"] = $row;
            $passItems[$id] = $v;
            $passIdKeyMap[$id] = $k;
        }
        if ($passItems && $this->rowsCallback) {
            $error = call_user_func_array($this->rowsCallback, [$passItems]);
            foreach ($passItems as $id => $item) {
                $idx = $passIdKeyMap[$id];
                $itemError = is_string($error) ? $error : (is_array($error) && array_key_exists($id, $error) && is_string($error[$id]) ? $error[$id] : null);
                $this->setImportItemState($items[$idx], $itemError);
            }
        }
        $response = [
            'limit' => $this->limit,
            'count' => $data['count'],
            'items' => $items
        ];
        return $response;
    }

    /**
     * 设置导入数据状态
     * @param array $item
     * @param string|null $error 错误描述，空默认为成功
     */
    private function setImportItemState(array &$item, string $error = null): void {
        if (is_string($error)) {
            $code = "error";
            $tips = "上传失败" . ($error ? ":{$error}" : "");
        } else {
            $code = "success";
            $tips = "上传成功";
        }
        $item["state"]["code"] = $code;
        $item["state"]["tips"] = $tips;
    }

    /**
     * Notes: 异步处理导入数据提交验证
     * Author: Uncle-L
     * Date: 2021/10/21
     * Time: 14:44
     * @param Validate $validate
     * @param array $row
     * @param string|null $sheet
     * @return bool|string  [通过返回true，失败返回失败原因]
     */
    private function async_import_validate(Validate $validate, array &$row, string $sheet = null) {
        $colsValidateArgs = $this->getImportColsValidateArgs($sheet);
        if (is_string($colsValidateArgs)) return $colsValidateArgs;
        list($rule, $field) = $colsValidateArgs;
        if (!$rule) return true;
        if ($sheet !== null) $row["sheet"] = $sheet;
        $res = data_validate($validate, $rule, $field, $row, "row");
        if ($res !== true) return $validate->getError();
        return true;
    }

    /**
     * 获取导入表头的验证参数
     * @param string $sheet
     * @return array|mixed
     */
    private function getImportColsValidateArgs(string $sheet = null) {
        static $args;
        $args = $args ?: [];
        if ($sheet !== null) {
            if ($args && isset($args[$sheet])) return $args[$sheet];
            if (!in_array($sheet, $this->sheet)) return "工具表{$sheet}错误";
            if (!isset($this->cols[$sheet])) return "工具表{$sheet}表头不存在";
            $cols = $this->cols[$sheet];
        } else {
            if ($args) return $args;
            $cols = $this->cols;
        }
        $rule = [];
        $field = [];
        foreach ($cols as $ck => $cv) {
            if (!isset($cv['verify']) || !$cv['verify']) continue;
            $rule[$ck] = $cv["verify"];
            $field[$ck] = $cv['title'] ?: $ck;
        }
        $colsArgs = [$rule, $field];
        if ($sheet) {
            $args[$sheet] = $colsArgs;
        } else {
            $args = $colsArgs;
        }
        return $colsArgs;
    }

}